import pypjl,random,re,time,hashlib
import threading
import pdb
class Scanner(threading.Thread):
    """
    All functions here are used for
    scanning for command support
    """
    def __init__(self,ip,port, scanEngine):
        threading.Thread.__init__(self)
        self.ip         = ip
        self.port       = port
        self.plib       = pypjl.PJLDynamic(True) #True means pad with random comments
        self.randomNap  = True #turn on random sleeps in the functions 
        self.scanEngine = scanEngine
        random.seed() #use system time as a random seed

    def scanModel(self):
        """
        Return Model number of a device by either 
        getModel or getModel2
        returns either model number as a string or False
        """
        functions = [self.plib.getModel,self.plib.getModel2,self.plib.getModel3]
        random.shuffle(functions)
        self.narcolepsy()
        for func in functions:
            rmodel = func(self.ip,self.port)
            if rmodel != False:
                return rmodel
            self.narcolepsy()
        return False

    def scanSerial(self):
        """
        Return Serial Number of a device by either
        getSerial or getSerial2
        returns either serial number as a string or False
        """
        functions = [self.plib.getSerial,self.plib.getSerial2]
        random.shuffle(functions)
        self.narcolepsy()
        for func in functions:
            rserial = func(self.ip,self.port)
            if rserial != False:
                return rserial
            self.narcolepsy()
            #print "serial Failed" for debug
        return False

    def scanDiskInfo(self):
        """
        Return printers Diskinfo
        Get the max space in ram of each file system
        as well as info about the file system
        Retruns a dictionary with info on all 
        of the supported file systems
        """
        self.narcolepsy()
        data = self.plib.filesys(self.ip,self.port)
        if data is False:
            return False
        m = re.compile('(?:@PJL INFO FILESYS\s*\[(\d) TABLE\]\s*VOLUME\s*TOTAL\s*SIZE\s*FREE\s*SPACE\s*LOCATION\s*LABEL\s*STATUS\s*(\d:)\s*(\d+)\s*(\d+)\s*(\w+\s+\d|\w+)\s*(\w+|\?)\s*([a-zA-Z0-9\-]+))+(?:\s*(\d:)\s*(\d+)\s*(\d+)\s*(\w+\s+\d|\w+)\s*(\w+|\?)\s*([a-zA-Z0-9\-]+))?(?:\s*(\d:)\s*(\d+)\s*(\d+)\s*(\w+\s+\d|\w+)\s*(\w+|\?)\s*([a-zA-Z0-9\-]+))?')

        z = m.match(data)
        if z is None:
            return False
        if z.group(1) is '2':
            p = {"status":z.group(7),
                    "label":z.group(6),
                    "location":z.group(5),
                    "freeSpace":z.group(4),
                    "totalSize":z.group(3),
                    "volume":z.group(2)}
        
        elif z.group(1) >= '3': #check for multiple disks
            #use what ever disk is larger
            if int(z.group(3)) > int(z.group(10)):
                p = {"status":z.group(7),
                    "label":z.group(6),
                    "location":z.group(5),
                    "freeSpace":z.group(4),
                    "totalSize":z.group(3),
                    "volume":z.group(2)}
            else:
                p = {"status":z.group(13),
                    "label":z.group(12),
                    "location":z.group(11),
                    "freeSpace":z.group(10),
                    "totalSize":z.group(9),
                    "volume":z.group(8)}
        return p

    def validFS(self,root='0:'):
        """
        check to see if a given printer is valid to use as 
        a file system
        """
        ranName = self.genFname()
        #randomize the info inside the test file with random size
        x = "".join(self.scanEngine.ranData[
            :random.randrange(2048,1048576)])
        self.narcolepsy()
        if self.plib.uploadFiles(x,root+"\\"+ranName,self.ip,self.port) == False: 
            #print "failed upload",self.ip #for debug
            return False
        else: sha = hashlib.sha256(x).hexdigest()   
        self.narcolepsy()
        y = self.plib.downloadFiles(root+"\\"+ranName,self.ip,self.port) 
        if y == False:
            #print "failed download",self.ip #for debug
            self.scanEngine.rescanUpdate(self.ip)
            return False
        self.narcolepsy()
        self.plib.delete(root+"\\"+ranName,self.ip,self.port)
        if hashlib.sha256(y).hexdigest() == sha:
            return True
        else: 
            #print "hash failed",self.ip #for debug
            self.scanEngine.rescanUpdate(self.ip)
            return False
    
    def narcolepsy(self):
        """
        Randomly Decide to sleep for a random amount of time
        """
        if self.randomNap is False:
            return
        if random.choice([1,2]) is 1:
            time.sleep(random.randrange(0,15)) #random sleep max 15
            return

    def genRanStr(self,bytelen):
        """
        generate a random string of data
        byelen = number of bytes to generate
        """
        letters = [] #empty list for mixed letters
        random.seed() #use system time as seed
        while len(letters) < bytelen:
            #generate some sudo random capital letters
            letters.append(chr(random.randrange(66,90,1)))
            letters.append(chr(random.randrange(97,122,1)))
        #join both lists and shuffle the letters
        random.shuffle(letters)
        return letters

    def wake(self):
        """
        try and send a wakeup command to the printer
        """
        self.plib.wakeup(self.ip,self.port)

    def genFname(self):
        """
        generate a random filename between 1 and 40 bytes
        should mean 4.36650282 x 10^68 possible unique file names
        """
        #return a random string name with random length
        return ''.join(random.sample(self.genRanStr(40),random.randint(1,40)))

    def FSscan(self):
        """
        Do the actual Scanning
        Return True or False if the device is supported
        """
        #store collected data
        printerInfo = {'port':self.port}
        scanfunctions = [self.scanSerial,self.scanDiskInfo,self.scanModel,self.wake]
        random.shuffle(scanfunctions)
        for function in scanfunctions:
            #loop though all the functions to wake up devices but we dont really care
            function()
            self.narcolepsy()
        random.shuffle(scanfunctions)
        #run though the now random data functions
        for function in scanfunctions:
            #function.__name__ gives us actual function name as str
            printerInfo[function.__name__] = function()
            if printerInfo[function.__name__] is False:
                if function.__name__ is 'scanModel':
                    continue
                else:
                    printerInfo['supported'] = False
                    return printerInfo
            self.narcolepsy()
        if self.validFS(printerInfo['scanDiskInfo']['volume']) is not False:
            #device is supported
            printerInfo['supported'] = True
            return printerInfo
        else:
            #device is not supported
            #print "validFS check failed %s" %(self.ip) #for debug
            printerInfo['supported'] = False
            return printerInfo
    
    def run(self):
        print "Scanning",self.ip
        try:
            if self.scanEngine.running.is_set():
                self.scanEngine.record(self.ip,self.FSscan())
        except KeyboardInterrupt:
            #if catch control c signal every one else to top
            self.scanEngine.cancel()
        #tell the engine we are finished
        self.scanEngine.finish(self)

class scanEngine:
    """
    class with all the controlls to do threaded scanning
    """
    def __init__(self,printers):
        """
        printers = list of IP's
        """
        #1meg of random chacters
        self.ranData  = Scanner('127.0.0.1',9100,self).genRanStr(1048576)
        #printers should be a list of ips
        self.printers = printers
        self.running  = threading.Event()
        self.safety   = threading.RLock()
        self.data     = {} #store found info here
        self.threads  = {} #store list of all thread objects
        self.rescan   = [] #printers that may have failed on hash, and need to rescan
        #default printer port
        self.port     = 9100 
    
    def rescanUpdate(self,ip):
        """
        add an IP to rescan due to possible hash failure
        """
        with self.safety:
            self.rescan.append(ip)

    def record(self,ip,data):
        """
        record the data found by the scanner
        ip = ip address of the printer scanned
        data = data returned by the scanner
        """
        with self.safety:
            self.data[ip] = data

    def cancel(self):
        """
        Clear the event flag that every thread checks
        With out this set the threads will exit on next check
        """
        print "Terminating..."
        self.running.clear()

    def finish(self, thread):
        """
        Remove the reference to the thread from list
        Of all running threads
        thread = ?
        """
        with self.safety:
            if thread.ip in self.threads:
                del self.threads[thread.ip]
            if thread.ip in self.rthreads:
                self.rthreads.remove(thread.ip)
    
    def total_thread_count(self):
        """
        count of all total threads to be run
        returns count as int
        """
        nthreads = 0
        with self.safety:
            nthreads = len(self.threads.keys())
        return nthreads
    
    def running_thread_count(self):
        """
        count of all current running threads
        returns count as int
        """
        nthreads = 0
        with self.safety:
            nthreads = len(self.rthreads)
        return nthreads

    def assign_work(self):
        with self.safety:
            for ip in self.threads.keys():
                if ip in self.rthreads:
                    continue
                else:
                    self.rthreads.append(ip)
                    self.threads[ip].start()
                #atempt to keep around 30 threads running
                if self.running_thread_count() <= self.MaxJobCount or self.running_thread_count() > self.MaxJobCount:
                    return
            #at this point there should be less then 30 peices of
            #work left to do in self.threads
            if self.total_thread_count() < self.MaxJobCount:
                return

    def main(self):
        """
        Makes the magic happen
        Decides how much work there is then que's up 
        and tracks the threads to do it
        """
        try:
            #set running event flag then que up 30 threads.
            self.running.set()
            #list of ips of running threads
            self.rthreads = []
            #set current max jobs
            self.MaxJobCount = 30
            #randomize the printer list a few times
            x = 0
            y = random.randrange(10,200)
            while x != y:
                random.shuffle(self.printers)
                x += 1
            #que up a list of all devices to run jobs on
            timer = time.time()
            for ip in self.printers:
                ip = ip.strip() #strip off newline
                self.threads[ip] = Scanner(ip, self.port, self)
                #start up 30 threads
                if self.running_thread_count() <= self.MaxJobCount:
                    self.rthreads.append(ip)
                    self.threads[ip].start()

            while self.running.is_set() and self.total_thread_count() >0:
                #print self.running_thread_count(), self.total_thread_count() #used for debugging showes running vs total threads to run
                if self.running_thread_count() <= self.MaxJobCount:
                    self.assign_work()
                time.sleep(1)
        except KeyboardInterrupt:
            #if catch of Ctrl-c signal all threads to stop
            self.cancel()
        #Wait for threads to stop but not more then 30sec
        timeout = 30
        while timeout > 0 and self.running_thread_count() > 0:
                time.sleep(1)
                timeout -= 1
        #debug why certain printers are coming up unsupported
        #print self.data
        return (self.data,time.time() - timer,self.rescan)

if __name__ == "__main__":
    """
    used for testing
    """
    f = open('loca_ip.txt','r')
    data = f.readlines()
    f.close()
    ipList = []
    for line in data:
        ipList.append(line.strip())
    x = scanEngine(ipList).main()   
    pdb.set_trace()
    print x
